在前面幾篇的範例中,除了使用 ChatCompletionService 搭配 ChatHistory 建立具有短期記憶對話歷史記錄的呼叫之外,另一個呼叫方式就是使用 Kernel.InvokePromptAsync,這個方式通常用於單回合的對話(當然如果真的想用於多回合具有短期記憶的情境,也是可以自行把對話資料做組裝重送,只是一般會建議這樣的需求就使用ChatCompletionService 搭配 ChatHistory來實現比較輕鬆),因此可以回顧一下過去的範例裡,Kernel.InvokePromptAsync 方法會傳入一段 Prompt 內容做為參數,然後取得 LLMs 生成的內容結果。而相同的情境,在 Semantic Kernel 中有其它不同的寫法,本篇內容主要說明這些不同寫法間的關係,以避免產生困惑。
先建立一個待用的 Kernel 物件,沒有掛載任何 Plugin,並且建立一段 prompt template 內容,其中包含上一篇提到的變數機制。
var builder = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
endpoint: Config.aoai_endpoint,
deploymentName: Config.aoai_deployment,
apiKey: Config.aoai_apiKey);
Kernel kernel = builder.Build();
var prompt = "推薦5個 '{{$city}}' 熱門旅遊景點,並且使用條列清單方式列出";
var func = kernel.CreateFunctionFromPrompt(prompt);
var result = await func.InvokeAsync(kernel, new() { ["city"] = "高雄" });
這個方式是先使用 kernel 物件 + prompt 產生 KernelFunction 物件,CreateFunctionFromPrompt方法會根據 prompt 內容建立 KernelFunction 物件。這裡所產生的 KernelFunction 與之前提到的 Kernel 掛載 Plugin 後,針對 Plugin 類別裡的方法產生的 KernelFunction 不一樣,雖然它們都是型別都是 KernelFunction,但其內部並不相同,一個是 Native Code,而另一個則是 prompt 內容(早期的 Semantic Kernel 版本稱為語意function,由 prompt 組成,提供 LLMs 本身能處理的作業)。
建立好之後,與 LLMs 互動則是透過呼叫 KernelFunction.InvokeAsync 方法,並把 Kernel 以及prompt template的參數值帶入,便能取得 LLMs 的生成回應。
var result = await kernel.InvokeAsync(func, new() { ["city"] = "高雄" });
這個寫法與上一個寫法剛好是相反,以 kernel 物件的角度呼叫 kernel 的 InvokeAsync 方法,傳入 KernelFunction 以及prompt template的參數值,便能取得 LLMs 的生成回應。而事實上kernel 的 InvokeAsync 方法的內部其實是呼叫了 KernelFunction.InvokeAsync 方法,因此這二種寫法只是從不同角度出發,但實際上運行方式是相同的。
var result = await kernel.InvokePromptAsync(prompt, new() { ["city"] = "高雄" });
同樣以 kernel 物件的角度出發,但傳入的是 prompt 以及prompt template的參數值,InvokePromptAsync 方法的內部呼叫了 Kernel.InvokeAsync,因此背後最終仍然是由 KernelFunction.InvokeAsync 方法處理,雖然 Kernel.InvokePromptAsync 看似沒有使用到 KernelFunction,但其實在內部使用 KernelFunctionFromPrompt.Create 方法建立了 KernelFunction。
經過對 Semantic Kernel 原始碼的深入挖掘,發現到儘管有不同寫法,但它們實際上是「殊途同歸」。這些不同的寫法只是基於軟體架構的設計,為了應對不同的應用情境而產生的變化。在這個挖掘過程中,也能進一步理解 Semantic Kernel 的核心概念,與大規模語言模型(LLMs)的互動視作一種「Function」的表現形式。與傳統應用程式中以程式語言撰寫的Function不同,這裡的「Function」是由 prompt 構建而成的。
因此,在實際開發中,選擇哪一種寫法並沒有絕對。例如,如果今天我們拿到的是一個已經配置好的 kernel(包括 Plugin),那麼最直接的方式就是使用 kernel.InvokePromptAsync。然而,若是需要不同的 kernel 搭配相同的 KernelFunction,那麼可能更適合使用 KernelFunction.InvokeAsync。本篇內容旨在透過這樣的分析,更具體的理解這些寫法上的差異,以便在未來參考其他人的範例或官方範例時,不至於感到困惑。